﻿//
// *************************************************************************************
// WP-Framework
// 
// Developed by
// Lukas Kretschmar (lkretschmar.dev@bluewin.ch)
//
// Switzerland, Copyright (c) 2013 lkretschmar
// This content is released under the MIT License (http://opensource.org/licenses/MIT).
//     
// *************************************************************************************
//
//
			

using System;
using System.Linq;
using System.Collections.Generic;
using System.Reflection;
using KretschIT.WP_Fx.EasyMoq.Generator.Arguments;
using KretschIT.WP_Fx.EasyMoq.Generator.Items;
using KretschIT.WP_Fx.Core.Validation;

namespace KretschIT.WP_Fx.EasyMoq.Generator.Constraints
{
    internal class GenericConstraintBuilder : IGenericConstraintBuilder
    {
        private const string ConstraintFormat = "where {0} : {1}";
        private const string InnerConstraintSeparator = ", ";
        private const string OuterConstraintSeparator = " ";
        private static readonly IDictionary<GenericParameterAttributes, string> ConstraintFor = new Dictionary<GenericParameterAttributes, string>
            {
                { GenericParameterAttributes.DefaultConstructorConstraint, "new()" },
                { GenericParameterAttributes.NotNullableValueTypeConstraint, "struct" },
                {GenericParameterAttributes.ReferenceTypeConstraint, "class" }
            };

        private INamingFactory nameFactory;

        public GenericConstraintBuilder(INamingFactory nameFactory)
        {
            this.nameFactory = nameFactory;
        }

        public void AddGenericType(Type genericType)
        {
            if (!genericType.IsGenericParameter)
            {
                return;
            }
            CreateConstraintFor(genericType);
        }

        private void CreateConstraintFor(Type genericType)
        {
            var constraint = this.BuildConstraint(genericType);
            if (!string.IsNullOrWhiteSpace(constraint))
            {
                var typeConstraint = string.Format(ConstraintFormat, genericType.Name, constraint);
                this.AppendToConstraint(typeConstraint);
            }            
        }

        private string BuildConstraint(Type genericType)
        {
            var constraints = new List<string>();
            this.AddClassOrStruct(genericType, constraints);
            this.AddTypeConstraints(genericType, constraints);
            this.AddCtorConstraint(genericType, constraints);
            return this.BuildConstraint(constraints, c => c);
        }

        #region ClassOrStruct

        private void AddClassOrStruct(Type genericType, List<string> constraints)
        {
 	        this.AddIfContained(GenericParameterAttributes.ReferenceTypeConstraint, genericType.GenericParameterAttributes, constraints);
            this.AddIfContained(GenericParameterAttributes.NotNullableValueTypeConstraint, genericType.GenericParameterAttributes, constraints);
        }

        #endregion

        #region Type

        private void AddTypeConstraints(Type genericType, List<string> constraints)
        {
            var types = genericType.GetGenericParameterConstraints();
            var genericConstraintTypes = types.Where(t => this.IsNotValueType(t));
            foreach (var t in genericConstraintTypes)
            {
                this.AddGenericTypeConstraint(t, constraints);
            }
        }
        
        private bool IsNotValueType(Type t)
        {
            return !t.Equals(typeof(ValueType));
        }

        private void AddGenericTypeConstraint(Type t, List<string> constraints)
        {   
            var name = this.nameFactory.GetNormalizedNameOf(t);         
            if (t.IsGenericType)
            {                
                name += this.BuildGenericArgumentsFor(t);                
            }
            constraints.Add(name);
        }

        private string BuildGenericArgumentsFor(Type type)
        {
            var builder = new GenericArgumentBuilder(this.nameFactory);
            foreach (var t in type.GetGenericArguments())
            {
                builder.Add(t);
            }
            return builder.String;
        }

        #endregion

        #region Ctor

        private void AddCtorConstraint(Type genericType, List<string> constraints)
        {
            if (this.Has(GenericParameterAttributes.NotNullableValueTypeConstraint, genericType.GenericParameterAttributes))
            {
                return;
            }
            this.AddIfContained(GenericParameterAttributes.DefaultConstructorConstraint, genericType.GenericParameterAttributes, constraints);
        }
        
        private void AddIfContained(GenericParameterAttributes attribute, GenericParameterAttributes attributes, IList<string> constraints)
        {
            if (this.Has(attribute, attributes) && ConstraintFor.ContainsKey(attribute))
            {
                constraints.Add(ConstraintFor[attribute]);
            }
        }

        private bool Has(GenericParameterAttributes attribute, GenericParameterAttributes attributes)
        {
            return (attributes & attribute) == attribute;
        }

        #endregion

        private string BuildConstraint<T>(IEnumerable<T> values, Func<T, string> valueOf)
        {
            if (values.Any())
            {
                var first = valueOf(values.First());
                return values.Skip(1).Aggregate(first, (str, v) => str + InnerConstraintSeparator + valueOf(v));
            }
            return string.Empty;
        }

        private void AppendToConstraint(string value)
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                return;
            }
            this.genericConstraint += OuterConstraintSeparator + value;
        }

        private string genericConstraint = string.Empty;

        public string GenericConstraint
        {
            get { return this.genericConstraint; }
        }
    }
}
